<

アクションとショートカットの使用

このページでは、物理キーボード イベントをユーザーのアクションにバインドする方法について説明します。 インターフェース。たとえば、アプリケーションでキーボード ショートカットを定義するには、次のようにします。 ページはあなたのためのものです。

概要

GUI アプリケーションが何かを行うには、ユーザーが伝えたいアクションが必要です。 へのアプリケーションする何か。アクションは多くの場合、次のような単純な関数です。 アクション (値の設定やファイルの保存など) を直接実行します。もっと大きなところでは ただし、アクションを呼び出すためのコードなど、物事はより複雑です。 また、アクション自体のコードは別の場所にある必要がある場合があります。 ショートカット(キーバインド)は何も知らないレベルでの定義が必要かも 彼らが引き起こすアクションについて。

そこで Flutter のアクションとショートカット システムが役に立ちます。 開発者は、それにバインドされたインテントを満たすアクションを定義できます。この中で コンテキストでは、インテントはユーザーが実行したい一般的なアクションであり、Intentクラス インスタンスは、Flutter でこれらのユーザー インテントを表します。アンIntent汎用的なものであり、さまざまな場所でさまざまなアクションによって実行されます。 コンテキスト。アンAction単純なコールバックにすることもできます (次の場合のように) のCallbackAction) または全体と統合するより複雑なもの 元に戻す/やり直しアーキテクチャ (たとえば) またはその他のロジック。

Using Shortcuts Diagram

Shortcutsキーまたは組み合わせを押すことによってアクティブ化されるキー バインディングです。 キーの。キーの組み合わせは、バインドされたインテントとともにテーブル内に存在します。いつ のShortcutsウィジェットがそれらを呼び出すと、一致するインテントが フルフィルメントのためのアクションサブシステム。

アクションとショートカットの概念を説明するために、この記事では ユーザーが両方を使用してテキスト フィールド内のテキストを選択およびコピーできるシンプルなアプリ ボタンとショートカット。

なぜアクションとインテントを分離するのでしょうか?

なぜキーの組み合わせをアクションに直接マッピングしないのかと疑問に思うかもしれません。なぜ そもそも意図があるのか​​?分離しておくと便利だからです。 キー マッピング定義がどこにあるか (多くの場合は高レベル)、 アクション定義がどこにあるか (多くの場合は低レベル)、そしてそれは 単一のキーの組み合わせを目的のキーにマップできることが重要です。 アプリ内での操作を制御し、どのアクションにも自動的に適応させます の意図された操作を実行します。 焦点を当てたコンテキスト。

たとえば、Flutter にはActivateIntentそれぞれのタイプをマッピングするウィジェット の対応するバージョンへのコントロールActivateAction(そしてそれは実行されます コントロールをアクティブにするコード)。このコードは多くの場合、かなりプライベートである必要があります 作業を行うためのアクセス権。追加の間接層が存在する場合、Intentが提供する 存在しなかった場合は、アクションの定義を次のレベルまで引き上げる必要があります。 ここで、の定義インスタンスは、Shortcutsウィジェットがそれらを認識できるため、 どのアクションを実行すべきかについて必要以上の知識を得る近道 呼び出し、必ずしも持っているとは限らない状態にアクセスしたり提供したりする またはそれ以外の必要があります。これにより、コードで 2 つの懸念事項をより明確に分離できるようになります。 独立。

インテントは、同じアクションが複数の用途に使用できるようにアクションを構成します。アン この例はDirectionalFocusIntent、移動する方向を指定します。 焦点を合わせて、DirectionalFocusActionどちらの方向に進むべきかを知るために フォーカスを移動します。注意してください: に状態を渡さないでください。Intentそれが当てはまる のすべての呼び出しに対してAction: そのような状態は に渡される必要があります。 のコンストラクタActionそれ自体、Intent知る必要があることから 過度に。

なぜコールバックを使用しないのでしょうか?

また、なぜコールバックの代わりにコールバックを使用しないのかと疑問に思うかもしれません。Action物体?主な理由は、アクションが、 を実装することで有効になりますisEnabled。また、キーがあれば役立つことがよくあります。 バインディングとそれらのバインディングの実装は別の場所にあります。

柔軟性のないコールバックだけが必要な場合は、ActionsShortcutsを使用できます。CallbackShortcutsウィジェット:

@override
Widget build(BuildContext context) {
  return CallbackShortcuts(
    bindings: <ShortcutActivator, VoidCallback>{
      const SingleActivator(LogicalKeyboardKey.arrowUp): () {
        setState(() => count = count + 1);
      },
      const SingleActivator(LogicalKeyboardKey.arrowDown): () {
        setState(() => count = count - 1);
      },
    },
    child: Focus(
      autofocus: true,
      child: Column(
        children: <Widget>[
          const Text('Press the up arrow key to add to the counter'),
          const Text('Press the down arrow key to subtract from the counter'),
          Text('count: $count'),
        ],
      ),
    ),
  );
}

ショートカット

以下でわかるように、アクションはそれ自体でも便利ですが、最も一般的な用途は この場合、それらをキーボード ショートカットにバインドする必要があります。これが、Shortcuts用のウィジェットです。

これは、ウィジェット階層に挿入され、キーの組み合わせを定義します。 そのキーの組み合わせが押されたときのユーザーの意図を表します。変換する 具体的なアクションへのキーの組み合わせの意図された目的、Actionsマッピングに使用されるウィジェットIntentAction。たとえば、次のことができます を定義するSelectAllIntent、それを自分のものにバインドしますSelectAllActionまたはあなたのCanvasSelectAllActionそして、その 1 つのキー バインディングから、システムは アプリケーションのどの部分にフォーカスがあるかに応じて、どちらかになります。方法を見てみましょう キーバインディング部分は機能します。

@override
Widget build(BuildContext context) {
  return Shortcuts(
    shortcuts: <LogicalKeySet, Intent>{
      LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyA):
          SelectAllIntent(),
    },
    child: Actions(
      dispatcher: LoggingActionDispatcher(),
      actions: <Type, Action<Intent>>{
        SelectAllIntent: SelectAllAction(model),
      },
      child: Builder(
        builder: (context) => TextButton(
          child: const Text('SELECT ALL'),
          onPressed: Actions.handler<SelectAllIntent>(
            context,
            SelectAllIntent(),
          ),
        ),
      ),
    ),
  );
}

人に渡された地図は、ShortcutsウィジェットマップLogicalKeySet(またはShortcutActivator、以下の注を参照)Intent実例。論理キー set は 1 つ以上のキーのセットを定義し、Intent は意図されたキーを示します。 キーを押す目的。のShortcutsウィジェットはマップ内のキー入力を検索します。 を見つけるためにIntentインスタンス、アクションに与えますinvoke()方法。

ショートカットマネージャー

ショートカット マネージャーは、Shortcutsウィジェット、パス 重要なイベントを受信したときにそれを実行します。方法を決定するためのロジックが含まれています。 キーを処理する、ツリーを上って他のショートカットを見つけるためのロジック マッピングを作成し、キーの組み合わせとインテントのマップを維持します。

のデフォルトの動作ですが、ShortcutManager通常は望ましいのですが、ShortcutsウィジェットはShortcutManagerサブクラス化してカスタマイズできること その機能性。

たとえば、各キーをログに記録したい場合、Shortcutsウィジェットが処理され、 を作ることができますLoggingShortcutManager:

class LoggingShortcutManager extends ShortcutManager {
  @override
  KeyEventResult handleKeypress(BuildContext context, RawKeyEvent event) {
    final KeyEventResult result = super.handleKeypress(context, event);
    if (result == KeyEventResult.handled) {
      print('Handled shortcut $event in $context');
    }
    return result;
  }
}

さて、毎回、Shortcutsウィジェットはショートカットを処理し、キーを出力します イベントと関連するコンテキスト。

行動

Actionsアプリケーションが実行できる操作の定義を可能にする で呼び出して実行します。Intent。アクションは有効または無効にすることができます。 それらを呼び出したインテント インスタンスを引数として受け取り、許可します。 意図による構成。

アクションの定義

アクションは、最も単純な形式では、次のサブクラスにすぎません。Action<Intent>invoke()方法。これは単に関数を呼び出す単純なアクションです。 提供されたモデル:

class SelectAllAction extends Action<SelectAllIntent> {
  SelectAllAction(this.model);

  final Model model;

  @override
  void invoke(covariant SelectAllIntent intent) => model.selectAll();
}

または、新しいクラスを作成するのが面倒な場合は、CallbackAction:

CallbackAction(onInvoke: (intent) => model.selectAll());

アクションを作成したら、次のコマンドを使用してそれをアプリケーションに追加します。Actionsウィジェット、地図を取得しますIntentにタイプしますActions:

@override
Widget build(BuildContext context) {
  return Actions(
    actions: <Type, Action<Intent>>{
      SelectAllIntent: SelectAllAction(model),
    },
    child: child,
  );
}

ShortcutsウィジェットはFocusウィジェットのコンテキストとActions.invokeに どのアクションを呼び出すかを見つけます。もしShortcutsウィジェットに一致するものが見つかりません 最初のインテントタイプActionsウィジェットが見つかると、次のウィジェットが考慮されます 祖先Actionsウィジェットなど、ウィジェットのルートに到達するまで続きます。 ツリーを参照するか、一致するインテント タイプを見つけて、対応するアクションを呼び出します。

アクションの呼び出し

アクション システムには、アクションを呼び出す方法がいくつかあります。これまでで最も一般的なのは 方法は、Shortcuts前のセクションで説明したウィジェット、 ただし、アクション サブシステムに問い合わせて、 アクション。キーにバインドされていないアクションを呼び出すことができます。

たとえば、インテントに関連付けられたアクションを見つけるには、次を使用できます。

Action<SelectAllIntent>? selectAll =
    Actions.maybeFind<SelectAllIntent>(context);

これは、Actionに関連するSelectAllIntentある場合は入力します 指定された場所で利用可能context。利用できない場合は null を返します。もしも 関連するAction常に利用可能である必要があるので、使用してくださいd5023989-d97f-475c-8e61-7a749558デダそれ以外のmaybeFind、一致するものが見つからない場合に例外をスローしますIntentタイプ。

アクション (存在する場合) を呼び出すには、次のように呼び出します。

Object? result;
if (selectAll != null) {
  result = Actions.of(context).invokeAction(selectAll, SelectAllIntent());
}

これを次の 1 つの呼び出しに結合します。

Object? result =
    Actions.maybeInvoke<SelectAllIntent>(context, SelectAllIntent());

場合によっては、ボタンを押したり、 別のコントロール。これを行うには、Actions.handlerを作成する関数 インテントに有効なアクションへのマッピングがある場合はハンドラー クロージャを返し、それを返します。 そうでない場合は null 。一致するものがない場合はボタンが無効になります。 コンテキスト内で有効なアクション:

@override
Widget build(BuildContext context) {
  return Actions(
    actions: <Type, Action<Intent>>{
      SelectAllIntent: SelectAllAction(model),
    },
    child: Builder(
      builder: (context) => TextButton(
        child: const Text('SELECT ALL'),
        onPressed: Actions.handler<SelectAllIntent>(
          context,
          SelectAllIntent(controller: controller),
        ),
      ),
    ),
  );
}

Actionsウィジェットは次の場合にのみアクションを呼び出しますisEnabled(Intent intent)true を返し、ディスパッチャがそれを考慮すべきかどうかをアクションが決定できるようにします。 呼び出しのために。アクションが有効になっていない場合、Actionsウィジェットが与える ウィジェット階層の上位にある別の有効なアクション (存在する場合) 実行する。

前の例では、BuilderなぜならActions.handlerActions.invoke(例) 提供されたアクションのみを検索しますcontext、 と 例が合格した場合、contextに与えられたbuild機能、フレームワーク 探し始めるその上現在のウィジェット。を使ってBuilderを許可します 同じフレームワークで定義されたアクションを見つけるためのフレームワークbuild関数。

アクションを必要とせずに呼び出すことができます。BuildContext、しかし、以来385a1ベッド-c863-4050-9579-50d46ce77061ウィジェットには、呼び出す有効なアクションを見つけるためのコンテキストが必要です。 独自のものを作成するか、提供する必要がありますActionインスタンス、または 適切なコンテキストでそれを見つけるActions.find

アクションを呼び出すには、アクションをinvokeのメソッドActionDispatcher、自分で作成したもの、または 既存 Actionsを使用したウィジェットActions.of(context)方法。かどうか確かめる アクションは呼び出す前に有効になっていますinvoke。もちろんお電話だけでも可能ですinvokeアクション自体に、Intent, しかし、その後はどれもオプトアウトします アクション ディスパッチャーが提供するサービス (ログ記録、元に戻す/やり直し、 すぐ)。

アクションディスパッチャ

ほとんどの場合、アクションを呼び出して、そのアクションを実行させたいだけです。 気にしないで。ただし、実行されたアクションをログに記録したい場合もあります。

ここでデフォルトを置き換えますActionDispatcherカスタムディスパッチャを使用する が入ってきます。ActionDispatcherにd331ad25-6e87-4dfc-a​​c8a-84ff13912bc3ウィジェットとそれ あらゆるものからアクションを呼び出しますActionsを設定しないウィジェットの下にあるウィジェット 独自のディスパッチャ。

最初のものActionsアクションを呼び出すときに行うのは、ActionDispatcherそして、呼び出しのためにアクションをそれに渡します。何もない場合は、 デフォルトが作成されますActionDispatcherそれは単にアクションを呼び出すだけです。

ただし、呼び出されたすべてのアクションのログが必要な場合は、独自のログを作成できます。LoggingActionDispatcher仕事をするために:

class LoggingActionDispatcher extends ActionDispatcher {
  @override
  Object? invokeAction(
    covariant Action<Intent> action,
    covariant Intent intent, [
    BuildContext? context,
  ]) {
    print('Action invoked: $action($intent) from $context');
    super.invokeAction(action, intent, context);

    return null;
  }
}

次に、それをトップレベルに渡しますActionsウィジェット:

@override
Widget build(BuildContext context) {
  return Actions(
    dispatcher: LoggingActionDispatcher(),
    actions: <Type, Action<Intent>>{
      SelectAllIntent: SelectAllAction(model),
    },
    child: Builder(
      builder: (context) => TextButton(
        child: const Text('SELECT ALL'),
        onPressed: Actions.handler<SelectAllIntent>(
          context,
          SelectAllIntent(),
        ),
      ),
    ),
  );
}

これにより、次のように、実行されるすべてのアクションがログに記録されます。

flutter: Action invoked: SelectAllAction#906fc(SelectAllIntent#a98e3) from Builder(dependencies: _[ActionsMarker])

それを一緒に入れて

の組み合わせActionsShortcuts強力です: ジェネリックを定義できます ウィジェット レベルで特定のアクションにマップされるインテント。シンプルなアプリはこちら これは、上で説明した概念を示しています。アプリはテキストフィールドを作成します。 隣には「すべて選択」ボタンと「クリップボードにコピー」ボタンもあります。ボタン 仕事を達成するためにアクションを呼び出します。呼び出されたすべてのアクションと ショートカットが記録されます。

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

/// A text field that also has buttons to select all the text and copy the
/// selected text to the clipboard.
class CopyableTextField extends StatefulWidget {
  const CopyableTextField({super.key, required this.title});

  final String title;

  @override
  State<CopyableTextField> createState() => _CopyableTextFieldState();
}

class _CopyableTextFieldState extends State<CopyableTextField> {
  late TextEditingController controller = TextEditingController();

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Actions(
      dispatcher: LoggingActionDispatcher(),
      actions: <Type, Action<Intent>>{
        ClearIntent: ClearAction(controller),
        CopyIntent: CopyAction(controller),
        SelectAllIntent: SelectAllAction(controller),
      },
      child: Builder(builder: (context) {
        return Scaffold(
          body: Center(
            child: Row(
              children: <Widget>[
                const Spacer(),
                Expanded(
                  child: TextField(controller: controller),
                ),
                IconButton(
                  icon: const Icon(Icons.copy),
                  onPressed:
                      Actions.handler<CopyIntent>(context, const CopyIntent()),
                ),
                IconButton(
                  icon: const Icon(Icons.select_all),
                  onPressed: Actions.handler<SelectAllIntent>(
                      context, const SelectAllIntent()),
                ),
                const Spacer(),
              ],
            ),
          ),
        );
      }),
    );
  }
}

/// A ShortcutManager that logs all keys that it handles.
class LoggingShortcutManager extends ShortcutManager {
  @override
  KeyEventResult handleKeypress(BuildContext context, RawKeyEvent event) {
    final KeyEventResult result = super.handleKeypress(context, event);
    if (result == KeyEventResult.handled) {
      print('Handled shortcut $event in $context');
    }
    return result;
  }
}

/// An ActionDispatcher that logs all the actions that it invokes.
class LoggingActionDispatcher extends ActionDispatcher {
  @override
  Object? invokeAction(
    covariant Action<Intent> action,
    covariant Intent intent, [
    BuildContext? context,
  ]) {
    print('Action invoked: $action($intent) from $context');
    super.invokeAction(action, intent, context);

    return null;
  }
}

/// An intent that is bound to ClearAction in order to clear its
/// TextEditingController.
class ClearIntent extends Intent {
  const ClearIntent();
}

/// An action that is bound to ClearIntent that clears its
/// TextEditingController.
class ClearAction extends Action<ClearIntent> {
  ClearAction(this.controller);

  final TextEditingController controller;

  @override
  Object? invoke(covariant ClearIntent intent) {
    controller.clear();

    return null;
  }
}

/// An intent that is bound to CopyAction to copy from its
/// TextEditingController.
class CopyIntent extends Intent {
  const CopyIntent();
}

/// An action that is bound to CopyIntent that copies the text in its
/// TextEditingController to the clipboard.
class CopyAction extends Action<CopyIntent> {
  CopyAction(this.controller);

  final TextEditingController controller;

  @override
  Object? invoke(covariant CopyIntent intent) {
    final String selectedString = controller.text.substring(
      controller.selection.baseOffset,
      controller.selection.extentOffset,
    );
    Clipboard.setData(ClipboardData(text: selectedString));

    return null;
  }
}

/// An intent that is bound to SelectAllAction to select all the text in its
/// controller.
class SelectAllIntent extends Intent {
  const SelectAllIntent();
}

/// An action that is bound to SelectAllAction that selects all text in its
/// TextEditingController.
class SelectAllAction extends Action<SelectAllIntent> {
  SelectAllAction(this.controller);

  final TextEditingController controller;

  @override
  Object? invoke(covariant SelectAllIntent intent) {
    controller.selection = controller.selection.copyWith(
      baseOffset: 0,
      extentOffset: controller.text.length,
      affinity: controller.selection.affinity,
    );

    return null;
  }
}

/// The top level application class.
///
/// Shortcuts defined here are in effect for the whole app,
/// although different widgets may fulfill them differently.
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  static const String title = 'Shortcuts and Actions Demo';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: title,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Shortcuts(
        shortcuts: <LogicalKeySet, Intent>{
          LogicalKeySet(LogicalKeyboardKey.escape): const ClearIntent(),
          LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyC):
              const CopyIntent(),
          LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyA):
              const SelectAllIntent(),
        },
        child: const CopyableTextField(title: title),
      ),
    );
  }
}

void main() => runApp(const MyApp());